|
1
|
|
|
import React, { useState, useMemo } from 'react'; |
|
2
|
|
|
import PropTypes from 'prop-types'; |
|
3
|
|
|
import { |
|
4
|
|
|
Card, |
|
5
|
|
|
CardHeader, |
|
6
|
|
|
CardBody, |
|
7
|
|
|
UncontrolledDropdown, |
|
8
|
|
|
DropdownToggle, |
|
9
|
|
|
DropdownMenu, |
|
10
|
|
|
DropdownItem, |
|
11
|
|
|
} from 'reactstrap'; |
|
12
|
|
|
import { Line } from 'react-chartjs-2'; |
|
13
|
|
|
import { reverse } from 'ramda'; |
|
14
|
|
|
import moment from 'moment'; |
|
15
|
|
|
import { VisitType } from '../types'; |
|
16
|
|
|
import { fillTheGaps } from '../../utils/helpers/visits'; |
|
17
|
|
|
import './LineCHartCard.scss'; |
|
18
|
|
|
|
|
19
|
2 |
|
const propTypes = { |
|
20
|
|
|
title: PropTypes.string, |
|
21
|
|
|
visits: PropTypes.arrayOf(VisitType), |
|
22
|
|
|
highlightedVisits: PropTypes.arrayOf(VisitType), |
|
23
|
|
|
}; |
|
24
|
|
|
|
|
25
|
2 |
|
const steps = [ |
|
26
|
|
|
{ value: 'monthly', menuText: 'Month' }, |
|
27
|
|
|
{ value: 'weekly', menuText: 'Week' }, |
|
28
|
|
|
{ value: 'daily', menuText: 'Day' }, |
|
29
|
|
|
{ value: 'hourly', menuText: 'Hour' }, |
|
30
|
|
|
]; |
|
31
|
|
|
|
|
32
|
2 |
|
const STEP_TO_DATE_FORMAT = { |
|
33
|
|
|
hourly: (date) => moment(date).format('YYYY-MM-DD HH:00'), |
|
34
|
|
|
daily: (date) => moment(date).format('YYYY-MM-DD'), |
|
35
|
|
|
weekly(date) { |
|
36
|
|
|
const firstWeekDay = moment(date).isoWeekday(1).format('YYYY-MM-DD'); |
|
37
|
|
|
const lastWeekDay = moment(date).isoWeekday(7).format('YYYY-MM-DD'); |
|
38
|
|
|
|
|
39
|
|
|
return `${firstWeekDay} - ${lastWeekDay}`; |
|
40
|
|
|
}, |
|
41
|
3 |
|
monthly: (date) => moment(date).format('YYYY-MM'), |
|
42
|
|
|
}; |
|
43
|
|
|
|
|
44
|
10 |
|
const groupVisitsByStep = (step, visits) => visits.reduce((acc, visit) => { |
|
45
|
3 |
|
const key = STEP_TO_DATE_FORMAT[step](visit.date); |
|
46
|
|
|
|
|
47
|
3 |
|
acc[key] = acc[key] ? acc[key] + 1 : 1; |
|
48
|
|
|
|
|
49
|
3 |
|
return acc; |
|
50
|
|
|
}, {}); |
|
51
|
|
|
|
|
52
|
6 |
|
const generateDataset = (stats, label, color) => ({ |
|
53
|
|
|
label, |
|
54
|
|
|
data: Object.values(stats), |
|
55
|
|
|
fill: false, |
|
56
|
|
|
lineTension: 0.2, |
|
57
|
|
|
borderColor: color, |
|
58
|
|
|
backgroundColor: color, |
|
59
|
|
|
}); |
|
60
|
|
|
|
|
61
|
2 |
|
const LineChartCard = ({ title, visits, highlightedVisits }) => { |
|
62
|
5 |
|
const [ step, setStep ] = useState(steps[0].value); |
|
63
|
5 |
|
const groupedVisits = useMemo(() => groupVisitsByStep(step, reverse(visits)), [ visits, step ]); |
|
64
|
5 |
|
const labels = useMemo(() => Object.keys(groupedVisits), [ groupedVisits ]); |
|
65
|
5 |
|
const groupedHighlighted = useMemo( |
|
66
|
5 |
|
() => fillTheGaps(groupVisitsByStep(step, reverse(highlightedVisits)), labels), |
|
67
|
|
|
[ highlightedVisits, step, labels ] |
|
68
|
|
|
); |
|
69
|
|
|
|
|
70
|
5 |
|
const data = { |
|
71
|
|
|
labels, |
|
72
|
|
|
datasets: [ |
|
73
|
|
|
generateDataset(groupedVisits, 'Visits', '#4696e5'), |
|
74
|
|
|
highlightedVisits.length > 0 && generateDataset(groupedHighlighted, 'Selected', '#F77F28'), |
|
75
|
|
|
].filter(Boolean), |
|
76
|
|
|
}; |
|
77
|
5 |
|
const options = { |
|
78
|
|
|
maintainAspectRatio: false, |
|
79
|
|
|
legend: { display: false }, |
|
80
|
|
|
scales: { |
|
81
|
|
|
yAxes: [ |
|
82
|
|
|
{ |
|
83
|
|
|
ticks: { beginAtZero: true, precision: 0 }, |
|
84
|
|
|
}, |
|
85
|
|
|
], |
|
86
|
|
|
}, |
|
87
|
|
|
}; |
|
88
|
|
|
|
|
89
|
5 |
|
return ( |
|
90
|
|
|
<Card> |
|
91
|
|
|
<CardHeader> |
|
92
|
|
|
{title} |
|
93
|
|
|
<div className="float-right"> |
|
94
|
|
|
<UncontrolledDropdown> |
|
95
|
|
|
<DropdownToggle caret color="link" className="btn-sm p-0"> |
|
96
|
|
|
Group by |
|
97
|
|
|
</DropdownToggle> |
|
98
|
|
|
<DropdownMenu right> |
|
99
|
|
|
{steps.map(({ menuText, value }) => ( |
|
100
|
20 |
|
<DropdownItem key={value} active={step === value} onClick={() => setStep(value)}> |
|
101
|
|
|
{menuText} |
|
102
|
|
|
</DropdownItem> |
|
103
|
|
|
))} |
|
104
|
|
|
</DropdownMenu> |
|
105
|
|
|
</UncontrolledDropdown> |
|
106
|
|
|
</div> |
|
107
|
|
|
</CardHeader> |
|
108
|
|
|
<CardBody className="line-chart-card__body"> |
|
109
|
|
|
<Line data={data} options={options} /> |
|
110
|
|
|
</CardBody> |
|
111
|
|
|
</Card> |
|
112
|
|
|
); |
|
113
|
|
|
}; |
|
114
|
|
|
|
|
115
|
2 |
|
LineChartCard.propTypes = propTypes; |
|
116
|
|
|
|
|
117
|
|
|
export default LineChartCard; |
|
118
|
|
|
|